package com.junjie.blibli.util;

import com.github.tobato.fastdfs.domain.fdfs.FileInfo;
import com.github.tobato.fastdfs.domain.fdfs.MetaData;
import com.github.tobato.fastdfs.domain.fdfs.StorePath;
import com.github.tobato.fastdfs.domain.proto.storage.DownloadCallback;
import com.github.tobato.fastdfs.service.AppendFileStorageClient;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import com.junjie.blibli.Exception.http.ConditionException;
import com.junjie.blibli.Selector.StorageSelector;
import io.netty.util.internal.StringUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.*;

@Component
public class FastDFSUtil implements StorageSelector {
    //专门用来传输普通文件的
    @Autowired
    private FastFileStorageClient fastFileStorageClient;

    //专用用来做大文件断点续传的
    @Autowired
    private AppendFileStorageClient appendFileStorageClient;

    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    @Value("${fdfs.http.storage-addr}")
    private String httpFdfsStorageAddr;

    //默认分组
    private static final String DEFAULT_GROUP = "group1";

    //PATH_KEY+MD5就可以对每个文件进行区分了
    private static final String PATH_KEY = "path-key:";

    private static final String UPLOADED_SIZE_KEY = "uploaded-size-key:";

    private static final String UPLOADED_NO_KEY = "uploaded-no-key:";

    //默认分片大小，1024*1024是1M，自己看情况定制，下面是2M
    private static final int SLICE_SIZE = 1024 * 1024 * 2;

    //获取文件类型


    //上传
    public String uploadCommonFile(MultipartFile file) throws Exception {
        //MetaData就是文件的一些信息，可写可不写
        Set<MetaData> metaDataSet = new HashSet<>();
        String fileType = this.getFileType(file);
        //上传需要参数是，流，文件大小，文件类型，文件信息元数据
        //返回值是存储对象，存了存储路径之类的
        StorePath storePath = fastFileStorageClient.uploadFile(file.getInputStream()
                , file.getSize(), fileType, metaDataSet);
        //返回存储路径即可
        return storePath.getPath();
    }

    public String uploadCommonFile(File file, String fileType) throws Exception {
        Set<MetaData> metaDataSet = new HashSet<>();
        StorePath storePath = fastFileStorageClient.uploadFile(new FileInputStream(file),
                file.length(), fileType, metaDataSet);
        return storePath.getPath();
    }

    //上传可以断点续传的文件，这个方法是用来上传第一个分片的
    public String uploadAppenderFile(MultipartFile file) throws IOException {
        String fileName = file.getName();
        String fileType = this.getFileType(file);
        //第一个参数是groupId(默认group1)，第二个是流，第三个是size，第四个是type
        StorePath storePath = appendFileStorageClient.uploadAppenderFile(DEFAULT_GROUP
                , file.getInputStream(), file.getSize(), fileType);
        return storePath.getPath();
    }

    @Override
    public void test() {
        System.out.println("test");
    }

    //offset就是偏移量，也就是我们要添加分片内容的位置，这个方法上传剩下所有分片
    public void modifyAppenderFile(MultipartFile file,String filePath,long offset) throws Exception {
        appendFileStorageClient.modifyFile(DEFAULT_GROUP
                ,filePath,file.getInputStream(), file.getSize(),offset);
    }

    /**
     * 通过分片来上传一个文件
     * @param file 文件
     * @param fileMd5 文件内容md5加密，来与其他文件区分
     * @param sliceNo 分片编号
     * @param totalSliceNo 总共分片数，帮助判断什么时候上传完毕
     * @return
     */
    public String uploadFileBySlices(MultipartFile file,String fileMd5, Integer sliceNo,Integer totalSliceNo) throws Exception {
        if(file==null || sliceNo==null ||totalSliceNo==null){
            throw new ConditionException("参数异常");
        }
        //下面三个参数都是redis里的key，下面这个就是文件上传路径
        String pathKey = PATH_KEY+fileMd5;
        //这个参数是所有已上传分片加起来的总大小Size，存的是多少字节这种，相当于offset
        String uploadedSizeKey = UPLOADED_SIZE_KEY+fileMd5;
        //用来记录当前已经上传了几个分片，用来比对是否文件全部上传
        String uploadedNoKey = UPLOADED_NO_KEY+fileMd5;

        //这里获取出当然已经上传的总大小，也可以说是偏移量
        String uploadedSizeStr = redisTemplate.opsForValue().get(uploadedSizeKey);
        //具体偏移量
        Long uploadedSize = 0L;
        //这里判断是为了判断，当前传的是第一分片还是其他分片
        // 如果这个key值为Null，说明之前没有上传过，那就说明肯定是第一个分片啊
        //刚好uploadedSize默认值为0
        if(!StringUtil.isNullOrEmpty(uploadedSizeStr)){
            uploadedSize = Long.valueOf(uploadedSizeStr);
        }
        String fileType = this.getFileType(file);

        //判断是上传的是第一个分片，还是其他分片
        if(sliceNo==1){
            //第一片上传处理
            String path = this.uploadAppenderFile(file);
            if(StringUtil.isNullOrEmpty(path)) throw new ConditionException("上传失败");
            //把Path存储到redis中
            redisTemplate.opsForValue().set(pathKey,path);
            //因为当前上传的是第一片，所以直接设置进去就好了
            redisTemplate.opsForValue().set(uploadedNoKey,"1");
        }else{
            //其他分片上传处理逻辑
            String filePath = redisTemplate.opsForValue().get(pathKey);
            if(StringUtil.isNullOrEmpty(filePath)) throw new ConditionException("上传失败");
            //计算偏移量后，开始上传
            this.modifyAppenderFile(file,filePath,uploadedSize);
            //让uploadedNoKey自增加一
            redisTemplate.opsForValue().increment(uploadedNoKey);
        }
        //修改当前文件已上传总大小，并存入redis
        //因为文件已经做分片处理了，所以直接getSize
        uploadedSize += file.getSize();
        redisTemplate.opsForValue().set(uploadedSizeKey, String.valueOf(uploadedSize));

        //如果所有分片上传完毕，清空redis里面相关的key和value
        String uploadedNoStr = redisTemplate.opsForValue().get(uploadedNoKey);
        Integer uploadedNo = Integer.valueOf(uploadedNoStr);
        //保存下文件路径，要返回的
        String resultPath = "";
        //如果两个参数是相同的，就该结束整个上传了
        if(totalSliceNo.equals(uploadedNo)){
            resultPath = redisTemplate.opsForValue().get(pathKey);
            //删除所有的redisKey，用集合的方式统一删除比较好
            List<String> keyList = Arrays.asList(uploadedNoKey, pathKey, uploadedSizeKey);
            redisTemplate.delete(keyList);
        }
        /*
            这里返回路径其实前端有两种做法
            1、前端拿到第一次分片的返回的路径后直接缓存起来，等全部上传后，直接拿出来用
            2、在最后一次上传成功后，删除所有key之前，将Path保存下来，并返回
            这里我们使用第二种方式
         */
        return resultPath;
    }

    //后端分片实现，其实应该交给前端来做，存在localStorage
    public void convertFileToSlices(MultipartFile multipartFile) throws Exception {
        String fileName = multipartFile.getOriginalFilename();
        String fileType = this.getFileType(multipartFile);
        //将MultipartFile转换成JAVA自带的文件类型
        File file = this.multipartFileToFile(multipartFile);
        //进行分片操作
        long fileLength = file.length();
        int count = 1;
        for (int i = 0; i < fileLength; i+=SLICE_SIZE) {
            //这个类可以读文件也可以写文件，特点是随机访问
            //随机访问就是随机跳到文件任意位置开始读写
            //第二个参数是权限，要么是rw，要么是两个之一，这里我们只需要读的权限
            RandomAccessFile randomAccessFile = new RandomAccessFile(file,"r");
            //定位到具体的小分片起始位置
            randomAccessFile.seek(i);
            //读取一定数量的数据
            byte[] bytes = new byte[SLICE_SIZE];
            //获取一下当前读了多少数据，因为最后一个分片不一定是我们指定的大小
            int len = randomAccessFile.read(bytes);
            //将临时文件保存起来
            String path = "F:\\iotest\\FastDFSTest\\"+count+"."+fileType;
            File slice = new File(path);
            FileOutputStream fos = new FileOutputStream(slice);
            fos.write(bytes,0,len);
            fos.close();
            randomAccessFile.close();
            count++;
        }
        //操作完后记得删掉file，注意不是删掉临时文件
        file.delete();
    }

    //将MultipartFile转换成JAVA自带的文件类型的方法
    public File multipartFileToFile(MultipartFile multipartFile) throws IOException {
        String originalFilename = multipartFile.getOriginalFilename();
        //originalFilename是带着名称和文件类型的，因此要用点来区分
        String[] fileName = originalFilename.split("\\.");
        //生成临时文件，然后将MultipartFile内容传到临时文件中去
        //第一个参数是临时文件名称，第二个参数是临时文件类型，但是要注意补个点
        File file = File.createTempFile(fileName[0],"."+fileName[1]);
        multipartFile.transferTo(file);
        return file;
    }

    //删除
    public void deleteFile(String filePath){
        fastFileStorageClient.deleteFile(filePath);
    }

    //在线视频播放
    public void ViewVideoOnLineBySlices(HttpServletRequest request,
                                        HttpServletResponse response,
                                        String path) throws Exception{
        //获取文件信息
        FileInfo fileInfo = fastFileStorageClient.queryFileInfo(DEFAULT_GROUP, path);
        //文件总大小
        long totalFileSize = fileInfo.getFileSize();
        //拼接一下实际要访问的文件路径
        String url = httpFdfsStorageAddr + path;
        //获取请求头所有的名字
        Enumeration<String> headerNames = request.getHeaderNames();

        //把请求头信息放到我们的Map集合中
        Map<String,Object> headers = new HashMap<>();
        //hasMoreElements()就是判断枚举里面还有没有数据，有就一直循环
        while(headerNames.hasMoreElements()){
            String header = headerNames.nextElement();
            //request.getHeader()是根据name获取请求头的value
            headers.put(header,request.getHeader(header));
        }
        //获取Range信息
        String rangeStr = request.getHeader("Range");
        String[] range;
        //如果没有Range信息，就进行初始赋值
        if(StringUtil.isNullOrEmpty(rangeStr)){
            rangeStr= "bytes=0-"+(totalFileSize-1);
        }
        //大概意思是，bytes=作为一部分，后面的以-来分割，又分成几部分
        range = rangeStr.split("bytes=|-");

        long begin = 0;
        //如果range.length>=2，就说明只有开始，没有结束
        if(range.length>=2){
            begin = Long.parseLong(range[1]);
        }
        //end默认是totalFileSize-1
        long end = totalFileSize-1;
        if(range.length>=3){
            end = Long.parseLong(range[2]);
        }

        //判断实际分片长度
        long len = (end-begin)+1;
        //设置响应头，标注起点和终点，以及文件总大小
        String contentRange = "bytes " + begin + "-" + end + "/" +totalFileSize;
        response.setHeader("Content-Range",contentRange);
        response.setHeader("Accept-Ranges","bytes");
        response.setHeader("Content-Type","video/mp4");
        response.setContentLength((int)len);
        //设置响应码是206
        response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
        //调用HttpUtil，输出流
        HttpUtil.get(url,headers,response);
    }

    //下载文件方法，下载到服务器本地
    public void downLoadFile(String url, String localPath) {
        fastFileStorageClient.downloadFile(DEFAULT_GROUP, url,
                new DownloadCallback<String>() {
                    @Override
                    public String recv(InputStream ins) throws IOException {
                        File file = new File(localPath);
                        OutputStream os = new FileOutputStream(file);
                        int len = 0;
                        byte[] buffer = new byte[1024];
                        while ((len = ins.read(buffer)) != -1) {
                            os.write(buffer, 0, len);
                        }
                        os.close();
                        ins.close();
                        return "success";
                    }
                });
    }
}
